Galileo Computing <openbook>
Galileo Computing - Programming the Net
Galileo Computing - Programming the Net

...powered by www.netzwerkartist.de...

Java 2 von Friedrich Esser
Designmuster und Zertifizierungswissen
Zum Katalog
gp Kapitel 12 Serialisierung
  gp 12.1 Serialisierung: Kommunikation auf Basis von Objekten
  gp 12.2 Grundlagen der Serialisierung
  gp 12.3 Übertragungs-Protokolle
  gp 12.4 Einfache Anpassungen von Serializable
  gp 12.5 Klassen-Evolution
  gp 12.6 Anpassungen der Object-Streams
  gp 12.7 Zusammenfassung
  gp 12.8 Testfragen20

Kapitel 12 Serialisierung

Serialisieren bzw. Deserialisieren von Objekten ist ein Teilbereich von Java-I/O, geht aber weit über den Rahmen der traditionellen Kommunikation hinaus.
Hinter dem Begriff »Serialisierung« verbergen sich diverse Ebenen der Kommunikation auf Basis von Objekten.
Im Idealfall kann man durch Markieren der Klasse einfach alles der JVM überlassen. Im anderen Fall übernimmt man den gesamten Prozess des Marshalings bzw. Unmarshalings komplett.
In jedem Fall muss man sich über die Konsequenzen seiner Entscheidung in Bezug auf Vererbung, referenzierte Klassen, Klassen-Versionen und Sicherheiten im Klaren sein.


Galileo Computing

12.1 Serialisierung: Kommunikation auf Basis von Objekten  downtop

Einführung

Serialisierung ist der synonym verwendete Begriff für Objekt-Streams, d.h. das Lesen und Schreiben von Objekten. Serialisierung ist nicht auf singuläre Objekte beschränkt, da diese ja wieder andere Objekte referenzieren können.

Übertragen von Objekt-Graphen

Von primitiven Typen einmal abgesehen, müssen beliebige Objekt-Gebilde, präziser Objekt-Graphen, in ihrem aktuellen Zustand serialisiert und in einem Kanal übertragen werden.

gp  Das Hauptziel der Serialisierung ist ein einfacher standardisierter Default-Mechanismus zur Übertragung von Objekten, der stufenweise erweitert oder auch ersetzt werden kann.

ObjectOutput(Stream)
ObjectInput(Stream)

Die Objekt-Kommunikation führt die JVM im Normalfall mit Hilfe von Instanzen der Klassen ObjectOutputStream bzw. ObjectInputStream aus. Die beiden zentralen Methoden in diesen beiden Klassen sind in den Interfaces ObjectOutput bzw. ObjectInput deklariert:

write/readObject:
Object-Byte-Stream

gp  writeObject(), das ein Objekt in einen Byte-Stream umwandelt.
gp  readObject(), das einen Byte-Stream in ein Objekt umwandelt.

Hierzu benötigen beide Methoden den vollen Zugriff auf das zu übertragende Objekt.

Da beide Klassen indirekt die Interfaces DataOutput bzw. DataInput implementieren, können sie auch alle primitiven Daten serialisieren.


Galileo Computing

12.2 Grundlagen der Serialisierung  downtop

Allen Protokollen und Mechanismen der Serialisierung liegt eine Prämisse zugrunde:

Objekt-Rekonstruktion aus einer Byte-Sequenz

gp  Der Empfänger muss anhand des erhaltenen Byte-Streams in der Lage sein, die ursprünglichen Objekte zu rekonstruieren.
gp  Dies sollte auch dann möglich sein, wenn bei der Deserialisierung nur eine kompatible Klassen-Version vorliegt.

Gerade der letzte Punkt ist für Klassenerweiterungen erforderlich, allerdings auch nicht einfach zu realisieren.

Serialisierungs-Framework

Das Framework besteht aus zwei Teilen (Abb. 12.1).

Framework zur Serialisierung


Abbildung
Abbildung 12.1   Interfaces und Klassen zur (De-)Serialisierung

Serialisierung beruht auf
Interfaces

Im Unterschied zum allgemeinen I/O- bzw. Streaming-Konzept (vgl. dazu Kapitel 11, Package java.io) beruht die Objekt-Kommunikation auf Interfaces. Somit ist auch eine komplett eigencodierte Serialisierung möglich.

Allgemein gilt:

gp  Klassen, deren Objekte serialisiert werden sollen, implementieren Serializable bzw. Externalizable.
gp  Klassen, die die eigentliche Kommunikation vornehmen, implementieren die Interfaces ObjectOutput bzw. ObjectInput.

Galileo Computing

12.2.1 Standard-Serialisierung  downtop

Zuerst werden die in der Plattform realisierten Standard-Serialisierungs-mechanismen besprochen, da sie zur Realisierung von Erweiterungen oder Ersatz unbedingt notwendig sind.

Das Marshaling bzw. Unmarshaling der Daten wird von Instanzen der Klasse ObjectOutputStream bzw. ObjectInputStream übernommen:

writeObject()
serialisiert

gp  Um ein Objekt zu serialisieren, wird es der Instanz-Methode writeObject() von ObjectOutputStream übergeben.

readObject() deserialisiert

gp  Um ein neues Objekt - eine Art von Klon - auf der anderen Seite des Kanals zu deserialisieren, wird es der Instanz-Methode readObject() von ObjectInputStream übergeben (siehe Abb. 12.2).

Abbildung
Abbildung 12.2   (De-)Serialisierung von Objekten

Prinzipielles Code-Muster

Zum Serialisieren verwendet man Referenzen der Interfaces ObjectOutput bzw. ObjectInput:

Standard-Code-Muster für Objekt-Übertragung

  // Senden eines Objekts serObjectIn der Klasse 
SerClass
  ObjectOutput oos= new ObjectOutputStream(byteOutStream);
  oos.writeObject(serObjectIn);
  // Empfang eines Objekts serObjectOut der Klasse 
SerClass
  ObjectInput ois= new ObjectInputStream(byteInStream);
  serObjectOut= (SerClass) ois.readObject(); 
                 

Der Cast in der letzten Zeile ist notwendig, da readObject() ein Object zurückliefert.

Grundsätzliche Serialisierungs-Regeln

Icon
Grundlegende Serialisierungs-Regeln

1. Es werden nur Objekte serialisiert, keine statischen (Klassen-)Felder.
2. Die Klassen dieser Objekte müssen eines der Interfaces Serializable oder Externalizable implementieren.
3. Findet die Kommunikation zwischen zwei unterschiedlichen Prozessen statt, müssen beide JVMs Zugriff auf die Klasse des Objekts haben, da kein Byte-Code der Klasse übertragen wird.

Zu 1: Ein Objekt kann nur deserialisiert werden, wenn seine Klasse geladen ist. Dann sind aber bereits die statischen Felder der Klasse initialisiert.

Zu 2: Bei Serializable handelt es sich um ein Marker-Interface.

Zu 3: Bei unterschiedlichen Prozessen, z.B. Netzwerkübertragung von Objekten, muss sichergestellt sein, dass die andere JVM ebenfalls Zugriff auf (kompatible) Klassen der übertragenen Objekte hat.

Primitive Typen

Serialisieren von Daten primitiver Typen

Neben Objekten können mit Hilfe der Methoden write<X>() und read<X>() der Interfaces DataOutput und DataInput auch primitive Typen übertragen werden (siehe Abb. 12.1 und 12.2).


Galileo Computing

12.3 Übertragungs-Protokolle  downtop

Selbst wenn nur Daten primitiver Typen übertragen werden, beruht die Serialisierung auf einem vereinbarten Protokoll.

Serialisierungs-Protokoll mit Header

gp  Das Serialisierungs-Protokoll legt anhand einer Grammatik den Aufbau eines Byte-Streams fest.

Den Anfang eines Byte-Streams bildet ein Header, der aus der short-Konstanten STREAM_MAGIC (0xaced) und der Version STREAM_VERSION (zurzeit aktuell 0x0005) besteht (siehe Beispiel in 12.2.2).


Galileo Computing

12.3.1 Protokoll zu primitiven Typen  downtop

Block-Data-Record für write<X>-Methoden

Die mittels write<X>() übertragenen Daten werden in einem Block, dem so genannten Block-Data-Record, mit Kennung und Länge übertragen.

Die verwendeten Symbole sind im Interface ObjectStreamConstants (Abb. 12.1) definiert.

Wie bereits bei der einfachen I/O kann es leider auch hier noch zu Übertragungsfehlern, d.h. Fehlinterpretationen der Daten kommen:

Fehlinterpretationen nicht ausgeschlossen

gp  Das Protokoll für die Serialisierung von Daten primitiver Typen mittels write<X>() bzw. read<X>() lässt keine zweifelsfreie Rekonstruktion zu.

Beispiele

Um Interna der Serialisierung besser zu verstehen, benötigt man eine Hex/ASCII-Darstellung der Byte-Streams. Hierzu verwenden wir im Folgenden die statische Methode toHexAsciiString() der Utility-Klasse Sniffer:

Sniffer:
Byte-Arrays in Hex/ASCII-
Darstellung

class Sniffer {
  private static char hex[]= {'0','1','2','3','4','5','6','7',
                              '8','9','a','b','c','d','e','f'};
  public static String toHexAsciiString 
(byte[] b,int lfAtPos){
    if (b==null || b.length==0) return "";
    StringBuffer sb= new StringBuffer(4*b.length);
    int i,r;
    for (i= 0; i<b.length;i++) {
      sb.append(hex[(b[i]>>>4)&0xF]).append(hex[b[i]&0xF])
                                    .append(' ');
      if((i+1)%lfAtPos==0) {
        for (int j=i-lfAtPos+1; j<=i; j++) {
          if (32<=b[j] && b[j]<=126) sb.append((char)b[j]);
          else sb.append('.');
        }
        sb.append('\n');
      }
    }
    if (0 < (r= b.length%lfAtPos)) {
      for (i= 0;i<(lfAtPos-r)*3;i++) sb.append(' ');
      for (i= b.length-r;i<b.length; i++) {
        if (32<=b[i] && b[i]<=126) sb.append((char)b[i]);
        else sb.append('.');
      }
    }
    else sb.deleteCharAt(sb.length()-1);
    return sb.toString();
  }
}

Die folgende Kommunikation mit ByteArrayOutputStream und ByteArrayInputStream ist rein speicherbasierend, sehr schnell und aus zwei Gründen interessant:

gp  Sie lässt die Analyse der Struktur des Byte-Streams durch die Sniffer-Klasse sehr einfach zu.
gp  Sie erlaubt ein einfaches Cloning im Sinne von Deep-Copy (siehe auch 6.10.2).

Byte-Stream eines Block-Data-Records

public class Test {
  public static void main(String[] args) {
    byte[] barr= null;
    ByteArrayOutputStream baos= new ByteArrayOutputStream();
    try {
      // schreibt bereits Header, siehe Erklärung unten
ObjectOutput oout= new ObjectOutputStream(baos); ¨
      oout.writeInt(7);
      oout.writeBoolean(false);
      oout.write((byte)49);
      oout.flush();             // schreibt nach baos

Sniffer im Einsatz

      // wandelt Byte-Stream in Byte-Array um
      barr= baos.toByteArray();
      // Hex/ASCII-Darstellung mit 16 Zeichen pro Zeile
      System.out.println(Sniffer.toHexAsciiString(barr,16));
      ObjectInput oin= new ObjectInputStream(
                         new ByteArrayInputStream(barr));
      System.out.println(oin.readInt());
      System.out.println(oin.readByte());                      ¦
      System.out.println(oin.readBoolean());                   Æ
      // System.out.println(oin.readChar()); 
                   Ø
    } catch (IOException e) { System.out.println(e); 
}
  }
}

Hex/ASCII:
Header, Block-Kennung und -Länge, Daten primitiver Typen

ac ed 00 05 77 06 00 00 00 07 00 31             
....w......1
7
0
true

Zu ¨: Wie am Anfang dieses Abschnitts beschrieben, bilden die ersten vier Bytes den Header. Dieser wird bereits bei der Anlage des ObjectOutputStream mit writeStreamHeader() herausgeschrieben. Deshalb muss die Anweisung im try-catch stehen.

Zum Block-Data-Record: Der Block-Anfang ist mit 77 (TC_BLOCKDATA) markiert, 06 ist dann die Block-Länge und anschließend folgen die Daten.

Zu write<X>, read<X>: Typ-Informationen sind im Stream nicht enthalten. Deshalb können die Zeilen ¦ und Æ z.B. auch durch die Zeile Ø ersetzt werden, ohne dass dieser Fehler erkannt werden kann. Dies würde dann das Zeichen 1 liefern.

Codierung von Zeichen

Zeichen-Codierung in UTF8

gp  Grundsätzlich werden alle Zeichen vom Typ char oder String als UTF8 im Stream codiert.

Galileo Computing

12.3.2 Protokolle zur Objekt-Serialisierung  downtop

Für Objekte ist das Protokoll sowie das Stream-Format verständlicherweise wesentlich komplexer.

Icon

Mit den Interfaces Serializable und Externalizable sind zwei unterschiedliche Protokolle verbunden, die festlegen wie Objekte übertragen werden. Zusätzlich zu den bereits o.a. grundsätzlichen Regeln gilt:

Grundlegende Protokoll-Regeln

1. Implementiert eine Klasse keine der beiden Interfaces, so führt der Versuch, ein Objekt dieser Klasse zu serialisieren, zu der Ausnahme NotSerializableException.
2. Für die Klassen, die Externalizable implementieren, überträgt das Protokoll nur den voll qualifizierten Klassennamen. Alle weiteren Informationen zu den Feldern müssen von der Klasse selbst im Byte-Stream mittels der beiden Methoden
    gp  public void writeExternal(ObjectOutput out)
    gp  public void readExternal(ObjectInput in)
im Interface Externalizable codiert werden.

Default-Serialisierung: Klassen
implementieren nichts außer Serializable

3. Für Klassen, die nur Serializable implementieren, legt das Protokoll automatisch die Reihenfolge und Identifizierung seiner Felder und seiner Superklassen fest, wobei
    gp  static deklarierte Klassen-Variablen nicht serialisiert werden
    gp  die Klasse die zu übertragenden Instanz-Felder selbst bestimmen kann

Da Externalizable das Interface Serializable erweitert (Abb. 12.1), ist dass »nur« in der letzten Regel wesentlich. Denn alle Klassen, die Externalizable implementieren, sind auch als Serializable markiert.

Externalizable vs. Serializable

Die zweite und dritte Regel hat eine wichtige Implikation:

Externalizable Klassen:
Einbahnstraße für Subklassen

gp  Für die Subklassen einer Klasse, die Externalizable implementiert, gibt es keine automatische Serialisierung mehr.

Der folgende Code ist syntaktisch korrekt, aber semantisch sinnlos:

  class E implements Externalizable         { /*...*/ 
}
  class D extends E implements Serializable 
{ /*...*/ }

Die Klasse D muss trotzdem die Serialisierung selbst übernehmen.


Galileo Computing

12.3.3 Protokoll zu Externalizable  downtop

Das Interface Externalizable deklariert zwei Methoden, die jeweils als Argument einen Object-Stream übergeben bekommen.

Externalizable:
kein Marker- Interface

public interface Externalizable extends Serializable 
{
  void writeExternal(ObjectOutput out) throws IOException;
  void readExternal (ObjectInput in)   throws IOException,
                                       ClassNotFoundException;
}

Aufgrund der Object-Streams out bzw. in stehen bei der Implementierung der Methoden somit alle Methoden zum Lesen und Schreiben von primitiven Typen und Objekten zur Verfügung.

Anforderungen an externalizable Klassen

Anforderungen an externalizable Klassen

Klassen, die bei der Objekt-Kommunikation eine vollständige Kontrolle über die Feld-Inhalte benötigen, müssen

gp  Externalizable mit den beiden Methoden writeExternal() bzw. readExternal() implementieren.
gp  einen public deklarierten No-Arg-Konstruktor enthalten.

Im Gegensatz zu rein Serializable-Klassen kann eine Externalizable-Klasse von außen beliebig instanziiert werden, was für das Design recht unangenehme Konsequenzen haben kann (siehe auch 12.4.2).

Protokoll-Ablauf zu Externalizable

Protokoll-Ablauf bei Externalizable

Da die Hauptarbeit der Objekt-Kommunikation bis auf den Klassennamen nicht automatisch erfolgt, ist das Protokoll eigentlich recht einfach.

Bei der Serialisierung wird

1. der voll qualifizierte Klassenname des Objekts ASCII-codiert (nicht in Unicode!) in den Stream geschrieben.3
2. die Methode writeExternal() der Klasse mit dem entsprechenenden ObjectOutput-Stream aufgerufen.

Bei der Deserialisierung wird umgekehrt

1. die Klasse anhand des im Stream enthaltenen Namens identifiziert.
2. eine Instanz der Klasse mit Hilfe des public No-Arg-Konstruktors erschaffen.
3. die Methode readExternal() der Klasse mit dem entsprechenenden ObjectInput-Stream aufgerufen.

Beliebige Manipulation der Feld-Informationen

Mit writeExternal() werden die gewünschten Felder des Objekts in den Stream geschrieben, wobei beliebige Manipulationen, z.B. Verschlüsselung der Daten, möglich sind.

Sollen interne Objekte übertragen werden, muss dies selbst kodiert werden.

Reihenfolge der Felder ist wichtig

Bei readExternal() ist dann darauf zu achten, dass die Reihenfolge der Felder exakt eingehalten wird und eventuelle Entschlüsselungen vorgenommen werden.

Externalizable und Felder von Superklassen

Externalizable:
Felder von Superklassen nicht implizit enthalten

gp  Die Felder aller Superklassen - selbst wenn diese als Serializable markiert sein sollten - müssen ebenfalls explizit übertragen werden.

Gemäß dem o.a. zweiten Punkt der Deserialisierung, werden zwar automatisch die Felder der Superklassen eines Objekts angelegt, dies sorgt aber nur für die Default-Initialisierung.

Enthält das zu übertragende Objekt davon abweichende Werte, sind diese ohne explizite Übertragung im deserialisierten Objekt nicht vorhanden.

Beispiel

Nachfolgend wird eine sehr einfache Klasse E angelegt:

package kap12;
import java.io.*;

Beispiel:
externalizable Klasse

class E implements Externalizable 
{
  byte b= 1;
  public E() {};                    // public notwendig!
  E(byte b)  { this.b= b; }
  public void writeExternal 
(ObjectOutput out) {
    try { out.writeByte(b); 
    } catch (Exception e) {System.out.println(e);} 
  }
  public void readExternal 
(ObjectInput in) {
    try { b= (byte) in.readByte(); 
    } catch (Exception e) {System.out.println(e);}
  }
}

Ein Objekt der Klasse E wird nun wieder (de-)serialisiert und der Byte-Stream mit Sniffer ausgegeben:

public class Test {
  public static void main(String[] args) {
    ObjectOutput oout= new ObjectOutputStream(baos);
    oout.writeObject(new E((byte)3)); oout.flush();
    barr= baos.toByteArray();
    System.out.println(Sniffer.toHexAsciiString(barr,16));
    ObjectInput oin= new ObjectInputStream(
                       new ByteArrayInputStream(barr));
    System.out.println(((E)oin.readObject()).b);
  }
}

Beispiel:
Protokoll- Bytes einer externalizable Klasse

ac ed 00 05 73 72 00 07 6b 61 70 31 32 2e 45 c1 
....sr..kap12.E.
b5 79 8a 99 51 65 29 0c 00 00 78 70 77 01 03 78 .y..Qe)...xpw..x
3

Erklärung: Nach dem obligatorischen 4-Byte-Header startet das Objekt im Stream mit TC_OBJECT (73).

Nach der Klassenkennung TC_CLASSDESC(72) folgt die Länge (0007) und der Name der Klasse (kap12.E) als ASCII-Zeichen.

Anschließend folgt ein 64-Bit-Hashcode (c1..29), der die Klassen eindeutig identifiziert.

Am Ende stehen dann die Feldwerte des Objekts in einem Block. In diesem Fall ist dies nur ein Byte (03), eingerahmt in TC_BLOCKDATA(77), Länge (01) und TC_ENDBLOCKDATA(78).

Fazit

Externalizable:
eigenes Protokoll zu Feldern

Auch bei Externalizable ist das Protokoll bis auf die eigentlichen Feld-Informationen vorgegeben. Die Daten der Felder sind aber frei manipulierbar.


Galileo Computing

12.3.4 Protokoll zu Serializable  downtop

Implementiert eine Klasse Serializable, so kann die Serialisierung völlig transparent von der JVM übernommen werden.

Icon

Allerdings stellt das Protokoll gewisse Anforderungen an die Objekte und ihre Klassen.

1. Anforderungen an eine Klasse

Anforderungen an serializable Klassen

Eine Klasse ist serializable , wenn

gp  keine ihrer Superklassen Externalizable implementiert und
gp  sie Zugriff auf den No-Arg-Konstruktor der ersten Superklasse hat, die nicht serialisierbar ist.

Anforderungen an serializable Objekte

2. Anforderungen an ein Objekt

Ein Objekt ist dann serializable, wenn

gp  seine Klasse serializable ist und
gp  seine Referenz-Felder entweder null sind oder Objekte von Klassen referenziert, die serialisierbar sind.

Default-Serialisierung

Default-
Serialisierung

Unter dem Begriff Default-Serialisierung versteht man das Standard-Protokoll einer serializable Klasse, die selbst keine Serialisierungs-Methoden implementiert.

It´s magic -
read/writeObject(): Zugriff auf private Felder

Das Standard-Protokoll verwendet hierzu writeObject() bzw. readObject() von ObjectOutputStream bzw. ObjectInputStream.

gp  Die Methoden writeObject() und readObject() haben dazu »magischen« Zugriff auch auf alle private deklarierten Felder der Objekte.

Prinzipieller Ablauf der Default-Serialisierung

Ablauf der Default-
Serialisierung

Der Serialisierungs-Prozess eines Objekts mit der Methode writeObject() läuft im Wesentlichen wie folgt ab :

1. Es werden der Klassen-Deskriptor, d.h. der Klassenname sowie die Namen aller nicht transient deklarierten Felder in den Stream geschrieben.
2. Die Werte der Felder werden mit der Methode defaultWriteObject() in den Stream geschrieben.

Transitive Hüllen-Operation

3. Dabei werden alle Objekte, die über Felder vom Objekt erreichbar sind, ebenfalls in den Stream geschrieben, wobei für Objekte, die bereits serialisiert sind, nur ein Handle abgelegt wird. Diese Operation wird kurz als transitive Hülle (transitive closure) bezeichnet.
4. Ist ein Objekt Instanz einer serializable Subklasse, wird für die serialisierbaren Superklassen ebenfalls writeObject() oder es werden ihre speziellen Serialisierungs-Methoden7 aufgerufen.

ObjectStreamClass zur Feldbeschreibung

ObjectStreamClass-Descriptor

Im Gegensatz zu Externalizable werden also zu jedem Objekt auch die Namen der übertragenen Felder im Stream geschrieben.

Zu Klassen- bzw. Feldbeschreibungen wird eine Instanz des Klassen-Deskriptors ObjectStreamClass serialisiert.

Standard-Objekte wie Object oder String werden allerdings nur als Ident in den Stream geschrieben (siehe Konstanten in ObjectStreamConstants).

Beispiel

Beispiel:
Klassen- Hierarchie mit Aggregation

Objekte der folgenden Klasse Square sind serializable, unabhängig davon, ob die Superklasse Figure serialisierbar ist oder nicht.

Point muss allerdings in jedem Fall serialisierbar sein (vgl. 12.3.4 »Anforderungen an ein Objekt«).


Abbildung
Abbildung 12.3   Klassen-Diagramm zum Beispiel
class Point implements 
Serializable {  int x,y; }
class Figure /* implements 
Serializable */ {             ¨
  protected Point base= new Point();
  public  Figure()             { this(0,0); }
  public  Figure(int x, int y) { base.x=x; base.y=y; }
}
class Square extends 
Figure implements Serializable {
  private Point p= new Point();
  transient public boolean clockwise= true;
  public Square (int x1, int y1, int x2, int y2) {
    super(x1,y1); p.x= x2; p.y= y2;
  }
  public String toString() {  
    return "["+ base.x+","+ base.y+";"+p.x+","+p.y+";"+
           clockwise+"]";
  }
}

Zu ¨: Klassen- bzw. Feld-Informationen und Werte zu Figure werden in den Stream nur aufgenommen, wenn die Klasse serialisierbar ist, z.B. Serializable implementiert.

Objekt-Default-Deserialisierung

Ablauf
der Default- Deserialisierung

Der Deserialisierungs-Prozess der Methode readObject() für ein Objekt läuft prinzipiell wie folgt ab:

1. Nach Deserialisierung der ObjectStreamClass-Instanz werden die Klassen- bzw. Feldbeschreibungen ausgewertet und die zugehörige Klasse wird im lokalen System geladen.
2. Es wird eine Instanz erzeugt.
    gp  Für externalizable Objekte wird der public No-Arg-Konstruktor aufgerufen und anschließend readExternal().
    gp  r serializable Objekte wird - falls notwendig - der No-Arg-Konstruktor der ersten nicht serialisierbaren Superklasse aufgerufen.
3. Die Felder werden ohne Aufruf eines Konstruktors oder Instanz-Initialisierers mit Hilfe der Methode defaultReadObject() mit Werten belegt.
    gp  Felder, für die es keinen Wert im Stream gibt, erhalten den Default-Wert.

Die neu erschaffene Instanz ist somit total unabhängig vom originalen Objekt. Abschließend noch eine Anmerkung:

EOFException bei Vertauschung

gp  Der Versuch, ein Objekt als primitiven Typ zu deserialisieren, führt zu der Ausnahme EOFException.

Beispiel

Die Ausgabe von Test hängt also davon ab, ob Figure (siehe oben, Zeile ¨) das Interface Serializable implementiert hat.

public class Test {
  public static void main(String[] args) {
    ByteArrayOutputStream baos= new ByteArrayOutputStream();
    ObjectOutput oout= new ObjectOutputStream(baos);
    oout.writeObject(new Square(1,2,3,4)); oout.flush();
    ObjectInput oin= new ObjectInputStream(
                       new ByteArrayInputStream(
                          baos.toByteArray()));
    System.out.println(((Square)oin.readObject())); 
    // Figure nicht serialisierbar :: [0,0;3,4;false]
// Figure serialisierbar :: [1,2;3,4;false]
} }

Galileo Computing

12.4 Einfache Anpassungen von Serializable  downtop

Anpassung der Serialisierungs-Mechanismen

Neben den Varianten Default-Serialisierung bzw. Serialisierung mittels Externalizable gibt es noch weitere - teilweise recht komplexe - Möglichkeiten der Protokoll-Anpassung.


Galileo Computing

12.4.1 serialPersistentFields: Ersatz für transient  downtop

serialPersistentFields überschreibt transient

Die Kennzeichnung der nicht serialisierbaren Instanz-Felder als transient ist zwar einfach, aber manchmal nicht flexibel genug.

Dieser Default-Mechanismus lässt sich mit Hilfe des speziellen private static final deklarierten Felds serialPerstistentFields innerhalb der serializable Klasse überschreiben.

serialPersistentFields: ein Code-Muster

gp  Das Feld serialPerstistentFields muss mit einem Array von ObjectStreamField-Instanzen initialisiert werden, die die Namen und Typen der serialisierbaren Instanz-Felder enthalten:
    class C implements Serializable {
      // ..Felder..
      private static final ObjectStreamField[] 
        serialPersistentFields = {
          new ObjectStreamField("fieldname", fieldType.class),
          //...
        };
      //...
    }

Beispiel

In der Klasse C werden trotz transient die Felder s und i serialisiert:

class C implements Serializable {
  transient String s= "abc";      // transient nutzlos
  transient int i= 2;             // dito
  private static final ObjectStreamField[] 
      serialPersistentFields = {
        new ObjectStreamField("s", String.class),
        new ObjectStreamField("i", int.class)
      };
}

Galileo Computing

12.4.2 Externalizable: Kapselung unmöglich   downtop

Keine Kapselung: gravierender Nachteil von Externalizable

Ein gravierender Nachteil der Implementierung von Externalizable liegt darin, dass die Methoden writeExternal() bzw. readExternal() public deklariert werden müssen.

gp  Die Externalizable-Methoden können nicht nur von der JVM zur Serialisierung, sondern für jeden anderen Zweck missbraucht werden.

Aus Sicht der Kapselung sind Anpassungen bzw. Erweiterungen des Default-Mechanismus sicherlich die bessere Wahl.


Galileo Computing

12.4.3 Klasseninterne Anpassung der Default-Serialisierung  downtop

Klassenintern read/write Object() überschreiben Default-Mechanismus

Mit Hilfe der klasseninternen private deklarierten Methoden writeObject() und readObject() kann eine serializable Klasse den Default-Mechanismus anpassen.

Dazu muss die Klasse diese Methoden mit folgender Signatur (mit oder ohne throws) implementieren:

class C implements Serializable {
defaultWriteObject(): Aufruf des Defaults

Code-Muster

  private void writeObject 
(ObjectOutputStream oout) 
               throws IOException {
    //...
    // oout.defaultWriteObject();
    //...
  }
  private void readObject 
(ObjectInputStream oin) 
               throws ClassNotFoundException, IOException {

defaultRead
Object(): Aufruf des Defaults

    //...
    // oin.defaultReadObject();
    //...
  }
}

Auch hier wieder:
It´s magic

Diese beiden Methoden sind wie ihre gleichnamigen Pendants in den beiden Object-Streams magisch.

gp  Da ObjectOutputStream bzw. ObjectInputStream als Argument übergeben wird, können mit Hilfe der Default-Methoden die Werte der »normalen« Felder serialisiert werden.

Beispiel (in drei Varianten)

Die Klasse C enthält jeweils drei Varianten zu writeObject() bzw. readObject(). Beide Methoden

read/writeObject():
drei interne Varianten

1. sind leer, d.h. ohne Anweisung.
2. rufen nur ihre Default-Methoden auf (zusätzlich Zeile ¨ bzw. Ø).
3. rufen ihre Default-Methoden auf und setzen Datum und Zeit des statischen Felds d (zusätzlich Zeile ¨ ¦ bzw. Æ Ø).
class C implements Serializable {
  static Date d;
  String s= "hi";
  int i= 7;
  private void writeObject (ObjectOutputStream oout) {
    try {
      // oout.defaultWriteObject();                           ¨
      // oout.writeObject(Calendar.getInstance().getTime());  ¦
    } catch (Exception e) {System.out.println(e);}
  }
  private void readObject (ObjectInputStream oin) {
    try {
      // oin.defaultReadObject();                             Æ
      // d= (Date) oin.readObject();                          Ø
    } catch (Exception e) {System.out.println(e);}
  }
  public String toString() { return d+","+s+","+i; }
}
public class Test {
  public static void main(String[] args) {
    byte[] barr= null;
    ByteArrayOutputStream baos= new ByteArrayOutputStream();
    ObjectOutput oout= new ObjectOutputStream(baos);
    oout.writeObject(new C()); oout.flush();
    barr= baos.toByteArray();
    System.out.println(Sniffer.toHexAsciiString(barr,16));
    ObjectInput oin= new ObjectInputStream(
                       new ByteArrayInputStream(
                                   baos.toByteArray()));
    System.out.println(oin.readObject());
  }
}

1. Variante

Es werden nur die Klassen- und Feld-Informationen übertragen, da eine Instanz der ObjectStreamClass serialisiert wird.

Es fehlen alle Werte. Die Ausgabe ist uninteressant und wird deshalb weggelassen.

2. Variante

Mit mehr Aufwand hat man praktisch die Default-Serialisierung nachgebildet. Auch der Stream-Inhalt ist nahezu identisch mit demjenigen der Default-Serialisierung.

ac ed 00 05 73 72 00 07 6b 61 70 31 32 2e 43 2c 
....sr..kap12.C,
92 61 0f 51 e2 bc ff 03 00 02 49 00 01 69 4c 00 .a.Q......I..iL.
01 73 74 00 12 4c 6a 61 76 61 2f 6c 61 6e 67 2f .st..Ljava/lang/
53 74 72 69 6e 67 3b 78 70 00 00 00 07 74 00 02 String;xp....t..
68 69 78                                        hix
null,hi,7

Es wurde nur das Stream-Flag SC_SERIALIZABLE (0x02) mit dem Stream-Flag SC_WRITE_METHOD (0x01) (per AND) zu 0x03 überlagert. Ein zusätzliches TC_ENDBLOCKDATA (0x78) terminiert die Werte.

3. Variante

Zusatz-
Funktionalität: Serialisieren von statischen Feldern

Eine zusätzliche Funktionalität ist eigentlich der Sinn der klasseninternen Implementation der beiden Methoden.

gp  In diesem Fall wird der Wert eines statischen Felds übertragen, was bei der Default-Serialisierung nicht möglich ist.10 
Thu Dec 07 20:31:34 GMT+01:00 2000,hi,7

Die Ausgabe zeigt die Übertragung und erfolgreiche Initialisierung des statischen Felds d, das in der zweiten Variante noch null war.


Galileo Computing

12.4.4 Broker-Pattern: Stream-Ersatzobjekte  downtop

Icon

Serialisierung ist ein zusätzlicher Dienst zu einer Klasse. Es ist nicht unbedingt vorteilhaft, diesen Dienst in der Klasse selbst zu implementieren.

Broker-Pattern: transparentes Service-Objekt

Der nachfolgende Mechanismus basiert auf dem Broker-Pattern.

gp  Ein Broker (Objekt-Makler) ist ein Service-Objekt, das für eine Klasse einen bestimmten Dienst transparent für die Clients abwickelt.

Im konkreten Fall wird der Serialisierungs-Dienst der Klasse nicht von ihr selbst abgewickelt, sondern einem Broker-Objekt überlassen.

gp  Es wird also kein Objekt der serializable Klasse selbst, sondern ein Broker-Objekt in den Stream geschrieben (Abb. 12.4).

Broker-Muster für die Serialisierung


Abbildung
Abbildung 12.4   Broker übernimmt Serialisierungs-Aufgabe

Vorteile eines Brokers

Broker-Vorteil

Die Klasse selbst wird vom Serialisierungs-Prozess entlastet. Die Aufgabe der Übernahme relevanter Objekt-Informationen sowie der Wiederherstellung des Objekts liegt ausschließlich beim Broker-Objekt.

Ohne die ursprüngliche Klasse ändern zu müssen, können in Broker-Objekten verschiedene Strategien der Übertragung angewendet werden.

Delegation an Broker

Broker-Delegation in der Server-Klasse:
writeReplace()

Damit der Broker-Mechanismus auch greift, muss die serialisierbare Klasse die folgende Methode (in der Regel transparent als private) implementieren:

   [public|protected|private] Object writeReplace() 
                              throws ObjectStreamException;

Wird nun ein Objekt dieser Klasse durch ObjectOutputStream serialisiert, ruft dieser die Methode writeReplace() auf und schreibt das Resultat als Broker-Objekt in den Byte-Stream.11 

gp  Da im Byte-Stream nur das Broker-Objekt enthalten ist, kann die Deserialisierung nur aus der Rekonstruktion des Broker-Objekts bestehen.

Die delegierende Klasse kann zwar auch Externalizable implementieren, dies ist aber nicht unbedingt sinnvoll, da sie dann zwei Methoden enthält, die nicht genutzt werden.

Broker-Implementation

Mechanismus in der Broker-Klasse:
readResolve()

Implementiert die serialisierbare Broker-Klasse die Methode

   [public|protected|private] Object readResolve() 
                              throws ObjectStreamException;

so ruft ObjectInputStream beim Deserialisieren die Methode readResolve() des Broker-Objekts auf.

Das Resultat dieser Methode ist dann in der Regel das rekonstruierte Objekt des Originals (der delegierenden Klasse).

Beziehung: Klasse vs. Broker

writeReplace() und readResolve() sind unabhängig

Die Methoden writeReplace() und readResolve() sind unabhängig voneinander, können somit auch einzeln eingesetzt werden.

Die Methode writeReplace() kann in einer serialisierbaren Klasse ohne Broker eingesetzt werden, um z.B. nur bestimmte Objekte der Klasse zu deserialisieren.

gp  Entgegen dem Eindruck der offiziellen Dokumentation kann das Broker-Objekt durchaus mit readResolve() ein beliebiges Objekt liefern, nicht unbedingt das der ursprünglichen Klasse.

Kritik

Broker-Lösung basiert nicht auf Interfaces

Der Broker-Mechanismus basiert nicht auf Interfaces, da die beiden Methoden dann nur public deklariert sein könnten.

Das aktuelle Interface-Konzept ist also - wieder einmal - nicht flexibel genug, d.h. wird hier durch Reflexion oder (magic) private-Zugriffe umgangen.

Beispiele

Die triviale Klasse C ist ihr eigener Broker, d.h., es wird nur wieder die Default-Serialisierung nachgebildet:

class C implements Serializable {
  private Object writeReplace() throws ObjectStreamException {
    return this;
  }
  private Object readResolve() throws ObjectStreamException {
    return this;
  }
}

Delegator-Klasse mit zwei Broker-Varianten

Zur Klasse Delegator werden mögliche Broker-Varianten vorgestellt:

class Delegator implements Serializable {
  private int i;
  public Delegator(int i) { this.i= i; }
  public int geti() { return i; }
  private Object writeReplace() throws ObjectStreamException {
    return new Broker(this);
  }
}

1. Broker-Variante

Der Broker deserialisiert einfach ein konstantes String-Objekt:

Deserialisierung eines konstanten String-Objekts

class Broker implements Serializable {
  public Broker(Delegator d) {}
  private Object readResolve() throws ObjectStreamException {
    return "Delegator?";
  }
}

2. Broker-Variante

Der Broker deserialisiert ein Delegator-Objekt im gleichen Zustand:

Deserialisierung eines Delegator-Objekts im gleichen Zustand

class Broker implements Serializable {
  private int i;
  public Broker(Delegator d) { i= d.geti()^0xaaaa; }
  private Object readResolve() throws ObjectStreamException {
    return new Delegator(i^0xaaaa);
  }
}

Galileo Computing

12.5 Klassen-Evolution  downtop

Klassen-Evolution: Versionswechsel nach
Serialisierung

Die Objekt-Kommunikation kann bedingt durch Zeitversatz (Serialisieren in Dateien) oder verschiedene JVMs auf unterschiedliche Klassen-Versionen treffen.

Natürlich trifft dieses Problem gleichermaßen auf serializable und externalizable Klassen zu. Die folgende Diskussion beschränkt sich aber ausschließlich auf Klassen, die Serializable implementieren.

SUID

(S)UID: Stream Unique Identifier

Serialisierte Objekte einer anderen Klassen-Version können nicht ohne spezielle Vorkehrungen deserialisiert werden.

Eindeutiger Hashcode der Klassen-Version

gp  Mit jedem Objekt wird nicht nur die zugehörige Klasse, sondern auch ein eindeutiges Ident - kurz UID - vom Typ long serialisiert, das nicht nur die Klasse, sondern auch jede Version der Klasse eindeutig identifiziert.

Icon

Das UID ist der Hashcode12  , berechnet aus allen relevanten Klassen-Informationen wie Name, Felder, Parameter, Modifier etc. und ändert sich somit mit jeder neuen Version.

Stream-
Kompatibilität

gp  Serializable Klassen mit gleichem Namen, die dieselbe UID haben, werden bei der (De-)Serialisierung als stream-kompatible angesehen.13 

Mit demselben UID wird ausgedrückt, dass die neue Klassen-Version den Client-Kontrakt der alten einhält (siehe Stream-Kompatibilität).

gp  Für stream-kompatible Versionen einer Klasse C kann man das UID manuell durch folgende Anweisung setzen:
   class C implements Serializable {
     // jede stream-kompatible Version hat 
dieselbe id

UID-Anweisung

     public static final long serialVersionUID= idL; 14 
     //...
   }

Der Wert von id ist nicht festgelegt, er muss nur identifizierend sein.

UID-Berechnung

Will man ein UID-konformes Ident vergeben, kann man diesen manuell durch das Utility-Programm serialver15  berechnen lassen oder im Programm mit Hilfe der Klasse ObjectStreamClass:

class C implements Serializable {

getSerialVersionUID()

  static public long uid() {
    return ObjectStreamClass.lookup(C.class)
                            .getSerialVersionUID();
} //... }

In der Praxis wird bereits die erste Version einer serializable Klasse, nachdem sie stabil ist, manuell mit einer UID versehen. Denn für jede Klasse, die kein serialVersionUID deklariert hat, wird sonst vor dem Streaming-Prozess der Hashcode berechnet.


Galileo Computing

12.5.1 Stream-Kompatibilität  downtop

Der Begriff stream-kompatibel soll im Weiteren kurz präzisiert werden. Es gibt hierzu zwei Aspekte, d.h. eine notwendige und eine hinreichende Bedingung.

Verhalten der Default-Serialisierung

Icon

Notwendig für eine stream-kompatible Klassen-Evolution ist die Frage, was die Default-Serialisierung beim Deserialisierungs-Prozess an Versionsänderungen toleriert:

Stream-kompatible Klassen

gp  Neue Felder: Sind Felder im Stream, d.h. in der alten Version, nicht enthalten, werden sie mit 0 oder null initialisiert (nicht mit ihren Initialisierungswerten!).
gp  Fehlende Felder: Felder im Stream, die es in der neuen Version nicht mehr gibt, werden einfach ignoriert.

Nicht toleriert, d.h. mit einer InvalidClassException bestraft, werden dagegen folgende Änderungen:

gp  Typ-Wechsel: Ein Feld mit gleichem Namen wechselt den Typ.
gp  Änderung der Serialisierungsart: Eine Klasse, die im Stream enthalten ist, ändert ihre Serialisierungsart.

Zusätzlich notwendiger Kontrakt